/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.harmony.javax.security.auth;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.DomainCombiner;
import java.security.Permission;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;

/**
 * The central class of the {@code javax.security.auth} package representing an
 * authenticated user or entity (both referred to as "subject"). IT defines also
 * the static methods that allow code to be run, and do modifications according
 * to the subject's permissions.
 * <p>
 * A subject has the following features:
 * <ul>
 * <li>A set of {@code Principal} objects specifying the identities bound to a
 * {@code Subject} that distinguish it.</li>
 * <li>Credentials (public and private) such as certificates, keys, or
 * authentication proofs such as tickets</li>
 * </ul>
 */
public final class Subject implements Serializable {

	private static final long serialVersionUID = -8308522755600156056L;

	private static final AuthPermission _AS = new AuthPermission("doAs"); //$NON-NLS-1$

	private static final AuthPermission _AS_PRIVILEGED = new AuthPermission(
			"doAsPrivileged"); //$NON-NLS-1$

	private static final AuthPermission _SUBJECT = new AuthPermission(
			"getSubject"); //$NON-NLS-1$

	private static final AuthPermission _PRINCIPALS = new AuthPermission(
			"modifyPrincipals"); //$NON-NLS-1$

	private static final AuthPermission _PRIVATE_CREDENTIALS = new AuthPermission(
			"modifyPrivateCredentials"); //$NON-NLS-1$

	private static final AuthPermission _PUBLIC_CREDENTIALS = new AuthPermission(
			"modifyPublicCredentials"); //$NON-NLS-1$

	private static final AuthPermission _READ_ONLY = new AuthPermission(
			"setReadOnly"); //$NON-NLS-1$

	private final Set<Principal> principals;

	private boolean readOnly;

	// set of private credentials
	private transient SecureSet<Object> privateCredentials;

	// set of public credentials
	private transient SecureSet<Object> publicCredentials;

	/**
	 * The default constructor initializing the sets of public and private
	 * credentials and principals with the empty set.
	 */
	public Subject() {
		super();
		principals = new SecureSet<Principal>(_PRINCIPALS);
		publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS);
		privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS);

		readOnly = false;
	}

	/**
	 * The constructor for the subject, setting its public and private
	 * credentials and principals according to the arguments.
	 * 
	 * @param readOnly
	 *            {@code true} if this {@code Subject} is read-only, thus
	 *            preventing any modifications to be done.
	 * @param subjPrincipals
	 *            the set of Principals that are attributed to this
	 *            {@code Subject}.
	 * @param pubCredentials
	 *            the set of public credentials that distinguish this
	 *            {@code Subject}.
	 * @param privCredentials
	 *            the set of private credentials that distinguish this
	 *            {@code Subject}.
	 */
	public Subject(boolean readOnly, Set<? extends Principal> subjPrincipals,
			Set<?> pubCredentials, Set<?> privCredentials) {

		if (subjPrincipals == null || pubCredentials == null
				|| privCredentials == null) {
			throw new NullPointerException();
		}

		principals = new SecureSet<Principal>(_PRINCIPALS, subjPrincipals);
		publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS,
				pubCredentials);
		privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS,
				privCredentials);

		this.readOnly = readOnly;
	}

	/**
	 * Runs the code defined by {@code action} using the permissions granted to
	 * the {@code Subject} itself and to the code as well.
	 * 
	 * @param subject
	 *            the distinguished {@code Subject}.
	 * @param action
	 *            the code to be run.
	 * @return the {@code Object} returned when running the {@code action}.
	 */
	public static Object doAs(Subject subject, PrivilegedAction<?> action) {

		checkPermission(_AS);

		return doAs_PrivilegedAction(subject, action,
				AccessController.getContext());
	}

	/**
	 * Run the code defined by {@code action} using the permissions granted to
	 * the {@code Subject} and to the code itself, additionally providing a more
	 * specific context.
	 * 
	 * @param subject
	 *            the distinguished {@code Subject}.
	 * @param action
	 *            the code to be run.
	 * @param context
	 *            the specific context in which the {@code action} is invoked.
	 *            if {@code null} a new {@link AccessControlContext} is
	 *            instantiated.
	 * @return the {@code Object} returned when running the {@code action}.
	 */
	public static Object doAsPrivileged(Subject subject,
			PrivilegedAction<?> action, AccessControlContext context) {

		checkPermission(_AS_PRIVILEGED);

		if (context == null) {
			return doAs_PrivilegedAction(subject, action,
					new AccessControlContext(new ProtectionDomain[0]));
		}
		return doAs_PrivilegedAction(subject, action, context);
	}

	// instantiates a new context and passes it to AccessController
	private static Object doAs_PrivilegedAction(Subject subject,
			PrivilegedAction<?> action, final AccessControlContext context) {

		AccessControlContext newContext;

		final SubjectDomainCombiner combiner;
		if (subject == null) {
			// performance optimization
			// if subject is null there is nothing to combine
			combiner = null;
		} else {
			combiner = new SubjectDomainCombiner(subject);
		}

		PrivilegedAction<AccessControlContext> dccAction = new PrivilegedAction<AccessControlContext>() {
			public AccessControlContext run() {

				return new AccessControlContext(context, combiner);
			}
		};

		newContext = (AccessControlContext) AccessController
				.doPrivileged(dccAction);

		return AccessController.doPrivileged(action, newContext);
	}

	/**
	 * Runs the code defined by {@code action} using the permissions granted to
	 * the subject and to the code itself.
	 * 
	 * @param subject
	 *            the distinguished {@code Subject}.
	 * @param action
	 *            the code to be run.
	 * @return the {@code Object} returned when running the {@code action}.
	 * @throws PrivilegedActionException
	 *             if running the {@code action} throws an exception.
	 */
	public static Object doAs(Subject subject,
			PrivilegedExceptionAction<?> action)
			throws PrivilegedActionException {

		checkPermission(_AS);

		return doAs_PrivilegedExceptionAction(subject, action,
				AccessController.getContext());
	}

	/**
	 * Runs the code defined by {@code action} using the permissions granted to
	 * the subject and to the code itself, additionally providing a more
	 * specific context.
	 * 
	 * @param subject
	 *            the distinguished {@code Subject}.
	 * @param action
	 *            the code to be run.
	 * @param context
	 *            the specific context in which the {@code action} is invoked.
	 *            if {@code null} a new {@link AccessControlContext} is
	 *            instantiated.
	 * @return the {@code Object} returned when running the {@code action}.
	 * @throws PrivilegedActionException
	 *             if running the {@code action} throws an exception.
	 */
	public static Object doAsPrivileged(Subject subject,
			PrivilegedExceptionAction<?> action, AccessControlContext context)
			throws PrivilegedActionException {

		checkPermission(_AS_PRIVILEGED);

		if (context == null) {
			return doAs_PrivilegedExceptionAction(subject, action,
					new AccessControlContext(new ProtectionDomain[0]));
		}
		return doAs_PrivilegedExceptionAction(subject, action, context);
	}

	// instantiates a new context and passes it to AccessController
	private static Object doAs_PrivilegedExceptionAction(Subject subject,
			PrivilegedExceptionAction<?> action,
			final AccessControlContext context)
			throws PrivilegedActionException {

		AccessControlContext newContext;

		final SubjectDomainCombiner combiner;
		if (subject == null) {
			// performance optimization
			// if subject is null there is nothing to combine
			combiner = null;
		} else {
			combiner = new SubjectDomainCombiner(subject);
		}

		PrivilegedAction<AccessControlContext> dccAction = new PrivilegedAction<AccessControlContext>() {
			public AccessControlContext run() {
				return new AccessControlContext(context, combiner);
			}
		};

		newContext = AccessController.doPrivileged(dccAction);

		return AccessController.doPrivileged(action, newContext);
	}

	/**
	 * Checks two Subjects for equality. More specifically if the principals,
	 * public and private credentials are equal, equality for two
	 * {@code Subjects} is implied.
	 * 
	 * @param obj
	 *            the {@code Object} checked for equality with this
	 *            {@code Subject}.
	 * @return {@code true} if the specified {@code Subject} is equal to this
	 *         one.
	 */
	@Override
	public boolean equals(Object obj) {

		if (this == obj) {
			return true;
		}

		if (obj == null || this.getClass() != obj.getClass()) {
			return false;
		}

		Subject that = (Subject) obj;

		if (principals.equals(that.principals)
				&& publicCredentials.equals(that.publicCredentials)
				&& privateCredentials.equals(that.privateCredentials)) {
			return true;
		}
		return false;
	}

	/**
	 * Returns this {@code Subject}'s {@link Principal}.
	 * 
	 * @return this {@code Subject}'s {@link Principal}.
	 */
	public Set<Principal> getPrincipals() {
		return principals;
	}

	/**
	 * Returns this {@code Subject}'s {@link Principal} which is a subclass of
	 * the {@code Class} provided.
	 * 
	 * @param c
	 *            the {@code Class} as a criteria which the {@code Principal}
	 *            returned must satisfy.
	 * @return this {@code Subject}'s {@link Principal}. Modifications to the
	 *         returned set of {@code Principal}s do not affect this
	 *         {@code Subject}'s set.
	 */
	public <T extends Principal> Set<T> getPrincipals(Class<T> c) {
		return ((SecureSet<Principal>) principals).get(c);
	}

	/**
	 * Returns the private credentials associated with this {@code Subject}.
	 * 
	 * @return the private credentials associated with this {@code Subject}.
	 */
	public Set<Object> getPrivateCredentials() {
		return privateCredentials;
	}

	/**
	 * Returns this {@code Subject}'s private credentials which are a subclass
	 * of the {@code Class} provided.
	 * 
	 * @param c
	 *            the {@code Class} as a criteria which the private credentials
	 *            returned must satisfy.
	 * @return this {@code Subject}'s private credentials. Modifications to the
	 *         returned set of credentials do not affect this {@code Subject}'s
	 *         credentials.
	 */
	public <T> Set<T> getPrivateCredentials(Class<T> c) {
		return privateCredentials.get(c);
	}

	/**
	 * Returns the public credentials associated with this {@code Subject}.
	 * 
	 * @return the public credentials associated with this {@code Subject}.
	 */
	public Set<Object> getPublicCredentials() {
		return publicCredentials;
	}

	/**
	 * Returns this {@code Subject}'s public credentials which are a subclass of
	 * the {@code Class} provided.
	 * 
	 * @param c
	 *            the {@code Class} as a criteria which the public credentials
	 *            returned must satisfy.
	 * @return this {@code Subject}'s public credentials. Modifications to the
	 *         returned set of credentials do not affect this {@code Subject}'s
	 *         credentials.
	 */
	public <T> Set<T> getPublicCredentials(Class<T> c) {
		return publicCredentials.get(c);
	}

	/**
	 * Returns a hash code of this {@code Subject}.
	 * 
	 * @return a hash code of this {@code Subject}.
	 */
	@Override
	public int hashCode() {
		return principals.hashCode() + privateCredentials.hashCode()
				+ publicCredentials.hashCode();
	}

	/**
	 * Prevents from modifications being done to the credentials and
	 * {@link Principal} sets. After setting it to read-only this
	 * {@code Subject} can not be made writable again. The destroy method on the
	 * credentials still works though.
	 */
	public void setReadOnly() {
		checkPermission(_READ_ONLY);

		readOnly = true;
	}

	/**
	 * Returns whether this {@code Subject} is read-only or not.
	 * 
	 * @return whether this {@code Subject} is read-only or not.
	 */
	public boolean isReadOnly() {
		return readOnly;
	}

	/**
	 * Returns a {@code String} representation of this {@code Subject}.
	 * 
	 * @return a {@code String} representation of this {@code Subject}.
	 */
	@Override
	public String toString() {

		StringBuilder buf = new StringBuilder("Subject:\n"); //$NON-NLS-1$

		Iterator<?> it = principals.iterator();
		while (it.hasNext()) {
			buf.append("\tPrincipal: "); //$NON-NLS-1$
			buf.append(it.next());
			buf.append('\n');
		}

		it = publicCredentials.iterator();
		while (it.hasNext()) {
			buf.append("\tPublic Credential: "); //$NON-NLS-1$
			buf.append(it.next());
			buf.append('\n');
		}

		int offset = buf.length() - 1;
		it = privateCredentials.iterator();
		try {
			while (it.hasNext()) {
				buf.append("\tPrivate Credential: "); //$NON-NLS-1$
				buf.append(it.next());
				buf.append('\n');
			}
		} catch (SecurityException e) {
			buf.delete(offset, buf.length());
			buf.append("\tPrivate Credentials: no accessible information\n"); //$NON-NLS-1$
		}
		return buf.toString();
	}

	private void readObject(ObjectInputStream in) throws IOException,
			ClassNotFoundException {

		in.defaultReadObject();

		publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS);
		privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS);
	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
	}

	/**
	 * Returns the {@code Subject} that was last associated with the
	 * {@code context} provided as argument.
	 * 
	 * @param context
	 *            the {@code context} that was associated with the
	 *            {@code Subject}.
	 * @return the {@code Subject} that was last associated with the
	 *         {@code context} provided as argument.
	 */
	public static Subject getSubject(final AccessControlContext context) {
		checkPermission(_SUBJECT);
		if (context == null) {
			throw new NullPointerException("auth.09"); //$NON-NLS-1$
		}
		PrivilegedAction<DomainCombiner> action = new PrivilegedAction<DomainCombiner>() {
			public DomainCombiner run() {
				return context.getDomainCombiner();
			}
		};
		DomainCombiner combiner = AccessController.doPrivileged(action);

		if ((combiner == null) || !(combiner instanceof SubjectDomainCombiner)) {
			return null;
		}
		return ((SubjectDomainCombiner) combiner).getSubject();
	}

	// checks passed permission
	private static void checkPermission(Permission p) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(p);
		}
	}

	// FIXME is used only in two places. remove?
	private void checkState() {
		if (readOnly) {
			throw new IllegalStateException("auth.0A"); //$NON-NLS-1$
		}
	}

	private final class SecureSet<SST> extends AbstractSet<SST> implements
			Serializable {

		/**
		 * Compatibility issue: see comments for setType variable
		 */
		private static final long serialVersionUID = 7911754171111800359L;

		private LinkedList<SST> elements;

		/*
		 * Is used to define a set type for serialization.
		 * 
		 * A type can be principal, priv. or pub. credential set. The spec.
		 * doesn't clearly says that priv. and pub. credential sets can be
		 * serialized and what classes they are. It is only possible to figure
		 * out from writeObject method comments that priv. credential set is
		 * serializable and it is an instance of SecureSet class. So pub.
		 * credential was implemented by analogy
		 * 
		 * Compatibility issue: the class follows its specified serial form.
		 * Also according to the serialization spec. adding new field is a
		 * compatible change. So is ok for principal set (because the default
		 * value for integer is zero). But priv. or pub. credential set it is
		 * not compatible because most probably other implementations resolve
		 * this issue in other way
		 */
		private int setType;

		// Defines principal set for serialization.
		private static final int SET_Principal = 0;

		// Defines private credential set for serialization.
		private static final int SET_PrivCred = 1;

		// Defines public credential set for serialization.
		private static final int SET_PubCred = 2;

		// permission required to modify set
		private transient AuthPermission permission;

		protected SecureSet(AuthPermission perm) {
			permission = perm;
			elements = new LinkedList<SST>();
		}

		// creates set from specified collection with specified permission
		// all collection elements are verified before adding
		protected SecureSet(AuthPermission perm, Collection<? extends SST> s) {
			this(perm);

			// Subject's constructor receives a Set, we can trusts if a set is
			// from bootclasspath,
			// and not to check whether it contains duplicates or not
			boolean trust = s.getClass().getClassLoader() == null;

			Iterator<? extends SST> it = s.iterator();
			while (it.hasNext()) {
				SST o = it.next();
				verifyElement(o);
				if (trust || !elements.contains(o)) {
					elements.add(o);
				}
			}
		}

		// verifies new set element
		private void verifyElement(Object o) {

			if (o == null) {
				throw new NullPointerException();
			}
			if (permission == _PRINCIPALS
					&& !(Principal.class.isAssignableFrom(o.getClass()))) {
				throw new IllegalArgumentException("auth.0B"); //$NON-NLS-1$
			}
		}

		/*
		 * verifies specified element, checks set state, and security permission
		 * to modify set before adding new element
		 */
		@Override
		public boolean add(SST o) {

			verifyElement(o);

			checkState();
			checkPermission(permission);

			if (!elements.contains(o)) {
				elements.add(o);
				return true;
			}
			return false;
		}

		// returns an instance of SecureIterator
		@Override
		public Iterator<SST> iterator() {

			if (permission == _PRIVATE_CREDENTIALS) {
				/*
				 * private credential set requires iterator with additional
				 * security check (PrivateCredentialPermission)
				 */
				return new SecureIterator(elements.iterator()) {
					/*
					 * checks permission to access next private credential moves
					 * to the next element even SecurityException was thrown
					 */
					@Override
					public SST next() {
						SST obj = iterator.next();
						checkPermission(new PrivateCredentialPermission(obj
								.getClass().getName(), principals));
						return obj;
					}
				};
			}
			return new SecureIterator(elements.iterator());
		}

		@Override
		public boolean retainAll(Collection<?> c) {

			if (c == null) {
				throw new NullPointerException();
			}
			return super.retainAll(c);
		}

		@Override
		public int size() {
			return elements.size();
		}

		/**
		 * return set with elements that are instances or subclasses of the
		 * specified class
		 */
		protected final <E> Set<E> get(final Class<E> c) {

			if (c == null) {
				throw new NullPointerException();
			}

			AbstractSet<E> s = new AbstractSet<E>() {
				private LinkedList<E> elements = new LinkedList<E>();

				@Override
				public boolean add(E o) {

					if (!c.isAssignableFrom(o.getClass())) {
						throw new IllegalArgumentException(
								"auth.0C " + c.getName()); //$NON-NLS-1$
					}

					if (elements.contains(o)) {
						return false;
					}
					elements.add(o);
					return true;
				}

				@Override
				public Iterator<E> iterator() {
					return elements.iterator();
				}

				@Override
				public boolean retainAll(Collection<?> c) {

					if (c == null) {
						throw new NullPointerException();
					}
					return super.retainAll(c);
				}

				@Override
				public int size() {
					return elements.size();
				}
			};

			// FIXME must have permissions for requested priv. credentials
			for (Iterator<SST> it = iterator(); it.hasNext();) {
				SST o = it.next();
				if (c.isAssignableFrom(o.getClass())) {
					s.add(c.cast(o));
				}
			}
			return s;
		}

		private void readObject(ObjectInputStream in) throws IOException,
				ClassNotFoundException {
			in.defaultReadObject();

			switch (setType) {
			case SET_Principal:
				permission = _PRINCIPALS;
				break;
			case SET_PrivCred:
				permission = _PRIVATE_CREDENTIALS;
				break;
			case SET_PubCred:
				permission = _PUBLIC_CREDENTIALS;
				break;
			default:
				throw new IllegalArgumentException();
			}

			Iterator<SST> it = elements.iterator();
			while (it.hasNext()) {
				verifyElement(it.next());
			}
		}

		private void writeObject(ObjectOutputStream out) throws IOException {

			if (permission == _PRIVATE_CREDENTIALS) {
				// does security check for each private credential
				for (Iterator<SST> it = iterator(); it.hasNext();) {
					it.next();
				}
				setType = SET_PrivCred;
			} else if (permission == _PRINCIPALS) {
				setType = SET_Principal;
			} else {
				setType = SET_PubCred;
			}

			out.defaultWriteObject();
		}

		/**
		 * Represents iterator for subject's secure set
		 */
		private class SecureIterator implements Iterator<SST> {
			protected Iterator<SST> iterator;

			protected SecureIterator(Iterator<SST> iterator) {
				this.iterator = iterator;
			}

			public boolean hasNext() {
				return iterator.hasNext();
			}

			public SST next() {
				return iterator.next();
			}

			/**
			 * checks set state, and security permission to modify set before
			 * removing current element
			 */
			public void remove() {
				checkState();
				checkPermission(permission);
				iterator.remove();
			}
		}
	}
}
